import pickle


def unpickle(obj):
    if obj is None:
        return None
    return pickle.loads(obj)


class TableBase(object):
    def __init__(self, *args, **kwargs):
        pass

    def get(self, key):
        raise NotImplementedError('Subclasses must implement `get` method.')

    def set(self, key, value):
        raise NotImplementedError('Subclasses must implement `set` method.')

    def delete(self, key):
        raise NotImplementedError('Subclasses must implement `delete` method.')

    def __getitem__(self, item):
        return self.get(item)

    def __setitem__(self, key, value):
        self.set(key, value)

    def __delitem__(self, key):
        self.delete(key)


class DBAdapterBase(object):
    table_class = TableBase

    def create_table(self, table, *args, **kwargs):
        raise NotImplementedError('Subclasses must implement `create_table` method.')

    def select_table(self, table):
        raise NotImplementedError('Subclasses must implement `select_table` method.')

    def drop_table(self, table):
        raise NotImplementedError('Subclasses must implement `drop_table` method.')

    def drop_database(self):
        raise NotImplementedError('Subclasses must implement `drop_database` method.')


class DictTable(TableBase):
    def __init__(self, *args, **kwargs):
        self._dict = dict()
        super().__init__(*args, **kwargs)

    def get(self, key, default=None):
        return self._dict.get(key, default)

    def set(self, key, value):
        self._dict[key] = value

    def delete(self, key):
        self._dict.pop(key)

    def __contains__(self, item):
        return item in self._dict

    def __getitem__(self, item):
        return self._dict[item]

    def __getattr__(self, attr):
        return getattr(self._dict, attr)

    def __iter__(self):
        return self._dict.__iter__()


class InMemoryAdapter(DBAdapterBase):
    table_class = DictTable

    def __init__(self):
        self._tables = {}

    def create_table(self, table, *args, **kwargs):
        new_table = self.table_class(*args, **kwargs)
        self._tables[table] = new_table

    def select_table(self, table):
        try:
            return self._tables[table]
        except KeyError:
            raise ValueError("Table '%s' not found." % table)

    def drop_table(self, table):
        self._tables.pop(table, None)

    def drop_database(self):
        self._tables.clear()


class RedisTable(TableBase):
    def __init__(self, redis, pattern, encoding='utf-8', *args, **kwargs):
        self._redis = redis
        self._pattern = pattern + ':'
        self.encoding = encoding
        super().__init__(*args, **kwargs)

    def serialize_key(self, key):
        serialized_key = '%s%s' % (self._pattern, key)
        return serialized_key.encode(self.encoding)

    def deserialize_key(self, serialized_key):
        if isinstance(serialized_key, bytes):
            serialized_key = serialized_key.decode(self.encoding)
        return serialized_key[len(self._pattern):]

    def get(self, key):
        key = self.serialize_key(key)
        value = self._redis.get(key)
        return unpickle(value)

    def set(self, key, value, *args, **kwargs):
        key = self.serialize_key(key)
        value = pickle.dumps(value)
        self._redis.set(key, value, *args, **kwargs)

    def delete(self, *keys):
        self._redis.delete(*keys)

    def keys(self):
        return map(self.deserialize_key, self._redis.scan_iter(self._pattern + '*'))

    def values(self):
        serialized_keys = self._redis.scan_iter(self._pattern + '*')
        return map(unpickle, self._redis.mget(serialized_keys))

    def items(self):
        serialized_keys = list(self._redis.scan_iter(self._pattern + '*'))
        keys = map(self.deserialize_key, serialized_keys)
        values = map(unpickle, self._redis.mget(serialized_keys))
        return zip(keys, values)

    def __contains__(self, key):
        return self._redis.exists(self.serialize_key(key))

    def __getattr__(self, attr):
        return getattr(self._redis, attr)

    def __iter__(self):
        return self.keys()


class RedisAdapter(DBAdapterBase):
    table_class = RedisTable

    def __init__(self, redis):
        self._redis = redis

    def create_table(self, table, *args, **kwargs):
        pass

    def select_table(self, table):
        return self.table_class(self._redis, table)

    def drop_table(self, table=None):
        for key in self._redis.scan_iter('%s:*' % table):
            self._redis.delete(key)

    def drop_database(self):
        self._redis.flushdb()
